/* Copyright (c) 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sohoappspot.sohosched.server.rpc;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import javax.jdo.Transaction;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.sohoappspot.sohosched.client.model.Note;
import com.sohoappspot.sohosched.client.model.Service;
import com.sohoappspot.sohosched.client.model.Surface;
import com.sohoappspot.sohosched.server.model.ModelParams;
import com.sohoappspot.sohosched.server.model.Store;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * The server-side RPC endpoint for {@link Service}.
 * 
 * @author knorton@google.com (Kelly Norton)
 * 
 */
@SuppressWarnings("serial")
public class ServiceImpl extends RemoteServiceServlet implements Service {

	private static final int TIMESTAMP_PADDING = 1000 * 60;

	private static Date convertTimestampToDate(String timetamp) {
		// To ensure we don't miss any events due to clock differences, we will
		// expand the time window by 1 minute.
		return new Date(Long.parseLong(timetamp, 16) - TIMESTAMP_PADDING);
	}

	private static String createTimestamp() {
		// The client should never need to do math on the timestamp and
		// returning a
		// long value into GWT just complicates the client side code since GWT
		// will
		// load code to emulate longs. To simplify things, we return a hex
		// encoded
		// string.
		return Long.toString(System.currentTimeMillis(), 16);
	}
	
	public UserInfoResult getUserInfo() {
		
		//Init
		final UserService userService = UserServiceFactory.getUserService();
		final User user = tryGetCurrentUser(userService);
		final Store.Api api = store.getApi();
		
		//Get User Info
		String userEmail = null;
		String userNickname = null;
		String redirectURL = "/SohoScheduler.html?gwt.codesvr=127.0.0.1:9997";
		String logoutURL = userService.createLogoutURL(redirectURL);
		String loginURL = userService.createLoginURL(redirectURL);
		Boolean isLoggedIn = false;
		Boolean isAdmin = false;

		//Assign Variables if user is logged in
		if(user!=null){
			userEmail = user.getEmail();
			userNickname = user.getNickname();
			isLoggedIn = userService.isUserLoggedIn();
			isAdmin = userService.isUserAdmin();
		}
		
		//Create Final Variable
		try {
		final UserInfoResult result = new Service.UserInfoResult(
				userEmail, userNickname, logoutURL, loginURL,isLoggedIn, isAdmin
				);
		return result;
		} finally {
			api.close();
		}
	}

	private static Note[] getNotesSinceTimestamp(Note[] notes, String timestamp) {
		if (timestamp == null) {
			return notes;
		} else {
			// Get the actual date + padding represented by the timestamp.
			final Date since = convertTimestampToDate(timestamp);
			final List<Note> newNotes = new ArrayList<Note>(notes.length);
			// Return only those notes that were updated after since.
			for (Note note : notes) {
				if (note.getLastUpdatedAt().after(since)) {
					newNotes.add(note);
				}
			}
			return newNotes.toArray(new Note[newNotes.size()]);
		}
	}

	private static String getSurfaceKey(Store.Note note) {
		// Use the fact that surface and note have parent/child keys to very
		// quickly
		// determine the surface key for a particular note.
		return KeyFactory.keyToString(note.getKey().getParent());
	}

	private static Note[] toClientNotes(Collection<Store.Note> notes) {
		final Note[] clients = new Note[notes.size()];
		int i = 0;
		for (Store.Note n : notes) {
			clients[i++] = new Note(KeyFactory.keyToString(n.getKey()), n
					.getX(), n.getY(), n.getWidth(), n.getHeight(), n
					.getContent(), n.getLastUpdatedAt(), n.getAuthorName(), n
					.getAuthorEmail());
		}
		return clients;
	}

	private static Surface toClientSurface(Store.Surface surface) {
		final List<String> names = surface.getAuthorNames();
		return new Surface(KeyFactory.keyToString(surface.getKey()), surface
				.getTitle(), names.toArray(new String[names.size()]), surface
				.getNotes().size(), surface.getLastUpdatedAt());
	}

	/**
	 * A convenient way to get the current user and throw an exception if the
	 * user isn't logged in.
	 * 
	 * @param userService
	 *            the user service to use
	 * @return the current user
	 * @throws AccessDeniedException
	 */
	private static User tryGetCurrentUser(UserService userService){
		User returnUser = null;
		if (userService.isUserLoggedIn()) {
			returnUser = userService.getCurrentUser(); 
			//throw new Service.AccessDeniedException();
		}
		return returnUser;
	}

	protected final Store store = ModelParams.getStore();
	
	/**
	 * A reference to a cache service.
	 */
	private final Cache cache = new Cache(MemcacheServiceFactory
			.getMemcacheService());

	public AddAuthorToSurfaceResult addAuthorToSurface(final String surfaceKey,
			final String email) throws AccessDeniedException {
		final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
		final Store.Api api = store.getApi();
		try {
			final Key key = KeyFactory.stringToKey(surfaceKey);

			final Store.Author me = api.getOrCreateNewAuthor(user);

			// Find an author with the given email address. If we can't find it,
			// we'll
			// return null to the client to indicate that the author does not
			// exist
			final Store.Author author = api.tryGetAuthor(email);
			if (author == null) {
				return null;
			}

			// Verify that author has access to the surface that is being
			// changed.
			if (!me.hasSurface(key)) {
				throw new Service.AccessDeniedException();
			}

			final Store.Surface surface = api.getSurface(key);
			// If the author already belongs to the surface, we return success
			// without
			// making any changes to the store.
			if (!author.hasSurface(key)) {

				cache.deleteSurfaceKeys(author.getEmail());
				cache.deleteSurface(surface.getKey());

				// Add the surface key to the author object. Since we'll be
				// updating an
				// object, carry out the operation in a transaction.
				final Transaction txA = api.begin();
				author.addSurface(surface);
				api.saveAuthor(author);
				txA.commit();

				// Add the author name to the surface.
				final Transaction txB = api.begin();
				surface.addAuthorName(author.getName());
				api.saveSurface(surface);
				txB.commit();
			}
			return new AddAuthorToSurfaceResult(author.getName(), surface
					.getLastUpdatedAt());

		} finally {
			api.close();
		}
	}

	public Date changeNoteContent(final String noteKey, final String content)
			throws AccessDeniedException {
		final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
		final Store.Api api = store.getApi();
		try {
			// Convert the string version of the key to an actual key.
			final Key key = KeyFactory.stringToKey(noteKey);
			final Store.Author me = api.getOrCreateNewAuthor(user);

			// Start a transaction for the Note we're updating.
			final Transaction tx = api.begin();
			final Store.Note note = api.getNote(key);
			// Verify that the author owns the Note.
			if (!note.isOwnedBy(me)) {
				throw new Service.AccessDeniedException();
			}
			note.setContent(content);
			final Date result = api.saveNote(note).getLastUpdatedAt();
			tx.commit();

			// Invalidate the notes cache for the surface that owns this Note.
			cache.deleteNotes(getSurfaceKey(note));
			return result;
		} finally {
			api.close();
		}
	}

	public Date changeNotePosition(final String noteKey, final int x,
			final int y, final int width, final int height)
			throws AccessDeniedException {
		final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
		final Store.Api api = store.getApi();
		try {
			// Convert the string version of the key into an actual key.
			final Key key = KeyFactory.stringToKey(noteKey);
			final Store.Author me = api.getOrCreateNewAuthor(user);

			// Start a transaction for the Note we're updating.
			final Transaction tx = api.begin();
			final Store.Note note = api.getNote(key);
			// Verify that the author owns the Note.
			if (!note.isOwnedBy(me)) {
				throw new Service.AccessDeniedException();
			}
			note.setX(x);
			note.setY(y);
			note.setWidth(width);
			note.setHeight(height);
			final Date result = api.saveNote(note).getLastUpdatedAt();
			tx.commit();

			// Invalidate the notes cache for the surface that owns this Note.
			cache.deleteNotes(getSurfaceKey(note));
			return result;
		} finally {
			api.close();
		}
	}

	public CreateObjectResult createNote(final String surfaceKey, final int x,
			final int y, final int width, final int height)
			throws AccessDeniedException {
		final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
		final Store.Api api = store.getApi();
		try {
			// Convert the string version of the key to the actual Key.
			final Key key = KeyFactory.stringToKey(surfaceKey);
			final Store.Author me = api.getOrCreateNewAuthor(user);

			// Verify that the author is actually a member of the surface.
			if (!me.hasSurface(key)) {
				throw new Service.AccessDeniedException();
			}

			// Start a transaction for the surface update.
			final Transaction tx = api.begin();
			final Store.Surface surface = api.getSurface(key);
			final Store.Note note = new Store.Note(me, x, y, width, height);
			surface.getNotes().add(note);
			api.saveSurface(surface);

			final CreateObjectResult result = new CreateObjectResult(KeyFactory
					.keyToString(note.getKey()), note.getLastUpdatedAt());
			tx.commit();

			// Invalidate the cache for the surface.
			cache.deleteNotes(surfaceKey);
			return result;
		} finally {
			api.close();
		}
	}

	public CreateObjectResult createSurface(final String title)
			throws AccessDeniedException {
		final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
		final Store.Api api = store.getApi();
		try {
			final Store.Author me = api.getOrCreateNewAuthor(user);

			final Store.Surface surface = new Store.Surface(title);
			surface.addAuthorName(me.getName());
			api.saveSurface(surface);

			final Transaction tx = api.begin();
			me.addSurface(surface);
			api.saveAuthor(me);
			tx.commit();

			// Invalidate the cached surface keys for this author.
			cache.deleteSurfaceKeys(me.getEmail());

			return new CreateObjectResult(KeyFactory.keyToString(surface
					.getKey()), surface.getLastUpdatedAt());
		} finally {
			api.close();
		}
	}

	public GetNotesResult getNotes(String surfaceKey, String since)
			throws AccessDeniedException {
		final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
		return new Service.GetNotesResult(createTimestamp(), getNotes(user,
				surfaceKey, since));
	}

	public GetSurfacesResult getSurfaces(String timestamp)
			throws AccessDeniedException {
		final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
		final Store.Api api = store.getApi();
		try {
			// getSurfaceKeys will return a cached entry if possible.
			final List<Key> keys = getSurfaceKeys(api, user);
			final Surface[] surfaces = new Surface[keys.size()];
			for (int i = 0, n = keys.size(); i < n; ++i) {
				// getSurface will return a cached entry if possible.
				surfaces[i] = getSurface(api, keys.get(i));
			}
			return new GetSurfacesResult(null, surfaces);
		} finally {
			api.close();
		}
	}



	private Note[] getNotes(User user, String surfaceKey, String since)
			throws AccessDeniedException {
		final Store.Api api = store.getApi();
		try {
			// Attempt to load from cache.
			final Note[] fromCache = cache.getNotes(user, surfaceKey);
			if (fromCache != null) {
				return getNotesSinceTimestamp(fromCache, since);
			}

			// Cache lookup failed, query the data store.
			final Key key = KeyFactory.stringToKey(surfaceKey);
			final Store.Author me = api.getOrCreateNewAuthor(user);
			if (!me.hasSurface(key)) {
				throw new Service.AccessDeniedException();
			}
			final Store.Surface surface = api.getSurface(key);
			final Note[] notes = cache.putNotes(user, surfaceKey,
					toClientNotes(surface.getNotes()));
			return getNotesSinceTimestamp(notes, since);
		} finally {
			api.close();
		}
	}

	private Surface getSurface(Store.Api api, Key key) {
		// Attempt to load from cache.
		final Surface fromCache = cache.getSurface(key);
		if (fromCache != null) {
			return fromCache;
		}

		// Cache lookup failed, query the data store.
		return cache.putSurface(key, toClientSurface(api.getSurface(key)));
	}

	private List<Key> getSurfaceKeys(Store.Api api, User user) {
		final String email = user.getEmail();

		// Attempt to load from cache.
		final List<Key> fromCache = cache.getSurfaceKeys(email);
		if (fromCache != null) {
			return fromCache;
		}

		// Cache lookup failed, query the data store.
		final Store.Author author = api.getOrCreateNewAuthor(user);
		return cache.putSurfaceKeys(email, author.getSurfaceKeys());
	}

}
